From 892ba9cf4a1eb40a06c0d1d8658afaf3d6369afd Mon Sep 17 00:00:00 2001 From: =?utf8?q?IOhannes=20m=20zm=C3=B6lnig=20=28Debian/GNU=29?= Date: Wed, 2 Sep 2026 00:20:45 +0200 Subject: [PATCH] New upstream version 3.0.1+ds --- AGENTS.md | 185 +++ CLAUDE.md | 1 + Dockerfile | 5 +- docs/Build/Linux.md | 2 +- docs/Documentation/ClientConnectionTypes.md | 387 ++++++ docs/Documentation/NetworkProtocol.md | 370 +++++ docs/changelog.yml | 38 + linux/Dockerfile.build | 21 +- linux/README.md | 2 +- linux/debug/README.md | 101 ++ linux/debug/crash.gdb | 39 + linux/debug/enable-core-dumps.sh | 45 + linux/debug/run-under-gdb.sh | 54 + linux/debug/run-with-coredump.sh | 62 + meson.build | 107 ++ meson_options.txt | 6 +- mkdocs.yml | 1 + plans/defer-webtransport-worker-creation.md | 160 +++ src/AudioInterface.cpp | 72 +- src/AudioInterface.h | 20 +- src/AudioTester.cpp | 4 +- src/Effects.h | 14 +- src/JMess.cpp | 9 +- src/JackAudioInterface.cpp | 90 +- src/JackAudioInterface.h | 22 +- src/JackTrip.cpp | 225 +++- src/JackTrip.h | 82 +- src/JackTripWorker.cpp | 414 +++++- src/JackTripWorker.h | 75 +- src/JitterBuffer.cpp | 3 + src/Limiter.h | 6 +- src/LoopBack.h | 2 +- src/OscServer.cpp | 2 +- src/PacketHeader.cpp | 68 + src/PacketHeader.h | 16 + src/ProcessPlugin.h | 4 +- src/Regulator.cpp | 129 +- src/Regulator.h | 6 +- src/Settings.cpp | 31 +- src/Settings.h | 4 + src/SocketClient.cpp | 12 +- src/SocketServer.cpp | 7 +- src/SslServer.cpp | 13 +- src/SslServer.h | 4 +- src/UdpDataProtocol.cpp | 40 +- src/UdpHubListener.cpp | 504 +++++-- src/UdpHubListener.h | 57 +- src/UserInterface.cpp | 7 +- src/gui/about.cpp | 2 +- src/gui/qjacktrip.cpp | 37 +- src/gui/qjacktrip.h | 13 + src/gui/textbuf.cpp | 65 +- src/gui/textbuf.h | 23 +- src/http3/Http3Protocol.cpp | 876 ++++++++++++ src/http3/Http3Protocol.h | 126 ++ src/http3/Http3Server.cpp | 318 +++++ src/http3/Http3Server.h | 120 ++ src/jacktrip_globals.cpp | 6 +- src/jacktrip_globals.h | 17 +- src/main.cpp | 39 + src/vs/AboutWindow.qml | 27 +- src/vs/Browse.qml | 442 +++--- src/vs/ChangeDevices.qml | 28 +- src/vs/Connected.qml | 3 - src/vs/CreateStudio.qml | 24 +- src/vs/DeviceControlsGroup.qml | 82 +- src/vs/DeviceRefreshButton.qml | 31 +- src/vs/DeviceWarningModal.qml | 47 +- src/vs/Failed.qml | 2 +- src/vs/FeedbackSurvey.qml | 52 +- src/vs/LearnMoreButton.qml | 34 +- src/vs/Login.qml | 2 +- src/vs/Permissions.qml | 45 +- src/vs/Recommendations.qml | 131 +- src/vs/Settings.qml | 125 +- src/vs/Setup.qml | 48 +- src/vs/StyledButton.qml | 54 + src/vs/arrow-left.svg | 4 + src/vs/arrow-right.svg | 4 + src/vs/arrow-top-right-on-square.svg | 4 + src/vs/home.svg | 4 + src/vs/question-mark-circle.svg | 3 + src/vs/squares-2x2.svg | 3 + src/vs/virtualstudio.cpp | 133 +- src/vs/virtualstudio.h | 3 +- src/vs/vs.qrc | 7 + src/vs/vsAudio.cpp | 28 +- src/vs/vsConstants.h | 3 + src/vs/vsDeviceCodeFlow.h | 2 +- src/webrtc/WebRtcDataProtocol.cpp | 533 ++++++++ src/webrtc/WebRtcDataProtocol.h | 185 +++ src/webrtc/WebRtcPeerConnection.cpp | 527 ++++++++ src/webrtc/WebRtcPeerConnection.h | 285 ++++ src/webrtc/WebRtcSignalingProtocol.cpp | 339 +++++ src/webrtc/WebRtcSignalingProtocol.h | 219 +++ src/webrtc/WebSocketSignalingConnection.cpp | 309 +++++ src/webrtc/WebSocketSignalingConnection.h | 163 +++ src/webtransport/WebTransportDataProtocol.cpp | 672 ++++++++++ src/webtransport/WebTransportDataProtocol.h | 214 +++ src/webtransport/WebTransportSession.cpp | 1189 +++++++++++++++++ src/webtransport/WebTransportSession.h | 314 +++++ subprojects/libdatachannel.wrap | 9 + subprojects/msquic.wrap | 10 + .../packagefiles/msquic/CMakeLists.txt | 961 +++++++++++++ subprojects/packagefiles/msquic/meson.build | 177 +++ .../packagefiles/msquic/meson_options.txt | 1 + .../msquic/src/bin/CMakeLists.txt | 301 +++++ 107 files changed, 11808 insertions(+), 1145 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 docs/Documentation/ClientConnectionTypes.md create mode 100644 docs/Documentation/NetworkProtocol.md create mode 100644 linux/debug/README.md create mode 100644 linux/debug/crash.gdb create mode 100755 linux/debug/enable-core-dumps.sh create mode 100755 linux/debug/run-under-gdb.sh create mode 100755 linux/debug/run-with-coredump.sh create mode 100644 plans/defer-webtransport-worker-creation.md create mode 100644 src/http3/Http3Protocol.cpp create mode 100644 src/http3/Http3Protocol.h create mode 100644 src/http3/Http3Server.cpp create mode 100644 src/http3/Http3Server.h create mode 100644 src/vs/StyledButton.qml create mode 100644 src/vs/arrow-left.svg create mode 100644 src/vs/arrow-right.svg create mode 100644 src/vs/arrow-top-right-on-square.svg create mode 100644 src/vs/home.svg create mode 100644 src/vs/question-mark-circle.svg create mode 100644 src/vs/squares-2x2.svg create mode 100644 src/webrtc/WebRtcDataProtocol.cpp create mode 100644 src/webrtc/WebRtcDataProtocol.h create mode 100644 src/webrtc/WebRtcPeerConnection.cpp create mode 100644 src/webrtc/WebRtcPeerConnection.h create mode 100644 src/webrtc/WebRtcSignalingProtocol.cpp create mode 100644 src/webrtc/WebRtcSignalingProtocol.h create mode 100644 src/webrtc/WebSocketSignalingConnection.cpp create mode 100644 src/webrtc/WebSocketSignalingConnection.h create mode 100644 src/webtransport/WebTransportDataProtocol.cpp create mode 100644 src/webtransport/WebTransportDataProtocol.h create mode 100644 src/webtransport/WebTransportSession.cpp create mode 100644 src/webtransport/WebTransportSession.h create mode 100644 subprojects/libdatachannel.wrap create mode 100644 subprojects/msquic.wrap create mode 100644 subprojects/packagefiles/msquic/CMakeLists.txt create mode 100644 subprojects/packagefiles/msquic/meson.build create mode 100644 subprojects/packagefiles/msquic/meson_options.txt create mode 100644 subprojects/packagefiles/msquic/src/bin/CMakeLists.txt diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..60f9091 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,185 @@ +## Project Overview + +JackTrip is a high-quality audio network performance system for bidirectional, uncompressed audio streaming over the Internet. It's a C++20 desktop application using Qt 6, supporting Linux, macOS and Windows. + +## Branching + +Always create new branches from the latest commit on the `dev` branch: + +```bash +git fetch origin +git checkout -b your-branch-name origin/dev +``` + +## Build Commands + +**Build system**: Meson (primary). CMake exists but is legacy/unsupported. + +```bash +# Configure (first time) +meson setup builddir + +# Build +meson compile -C builddir + +# Reconfigure with options +meson configure builddir -Dnogui=true # CLI-only build +meson configure builddir -Dnovs=true # without Virtual Studio +meson configure builddir -Dnoclassic=true # without classic Qt Widgets GUI + +# Clean rebuild +meson setup builddir --wipe +``` + +See `meson_options.txt` for setup options. + +## Code Formatting & Linting + +- **clang-format** (version 13) enforced in CI on all `src/**/*.{h,cpp}` files +- Style: Google-based, 90-column limit, 4-space indent, Linux brace style +- Run manually: `clang-format -i src/MyFile.cpp` +- **clang-tidy** runs as a separate CI check on PRs +- Pre-commit hook enforces clang-format + +## Testing + +Tests use Qt Test framework. Test files are in `tests/`: +- `tests/jacktrip_tests.cpp` — threading tests +- `tests/audio_socket_test.cpp` — AudioSocket tests + +Test coverage is minimal; the project relies primarily on manual/integration testing. + +## Architecture + +### Core Design (Mediator Pattern) + +**`JackTrip`** (`src/JackTrip.cpp`, ~3000 lines) is the central orchestrator coordinating all subsystems. + +**Data flow**: +- Sender: AudioInterface → RingBuffer → PacketHeader → UdpDataProtocol → Network +- Receiver: Network → UdpDataProtocol → JitterBuffer → RingBuffer → Effects → AudioInterface + +### Threading Model + +Four threads in operation: +1. **Audio thread** — real-time priority, driven by audio backend callbacks +2. **Network sender thread** — `DataProtocol` subclass +3. **Network receiver thread** — `DataProtocol` subclass +4. **GUI thread** — Qt event loop + +### Key Components + +| Component | Files | Purpose | +|-----------|-------|---------| +| Audio backends | `AudioInterface.cpp`, `JackAudioInterface.cpp`, `RtAudioInterface.cpp` | Abstract audio I/O (JACK or RtAudio) | +| Networking | `DataProtocol.cpp`, `UdpDataProtocol.cpp` | UDP-based audio transport | +| Buffering | `RingBuffer.cpp`, `JitterBuffer.cpp`, `Regulator.cpp` | Lock-free audio buffering and jitter management | +| Effects | `Compressor.cpp`, `Limiter.cpp`, `Reverb.cpp`, `Volume.cpp`, etc. | Audio processing via `ProcessPlugin` base class | +| Hub server | `UdpHubListener.cpp`, `JackTripWorker.cpp` | Multi-client server mode | +| Settings | `Settings.cpp` | Command-line parsing and configuration | +| Entry point | `main.cpp` | GUI/CLI dispatch | + +### GUI Modes + +- **Virtual Studio** (`src/vs/`) — Modern Qt QML + WebEngine GUI, requires Qt 6.2+. Main class: `VirtualStudio` +- **Classic** (`src/gui/`) — Traditional Qt Widgets GUI. Main class: `QJackTrip` +- **CLI** — Headless mode when built with `-Dnogui=true` + +### Extending Audio Effects + +Subclass `ProcessPlugin` and implement the `compute` method. Effects are chained in the audio processing pipeline by `JackTrip`. + +### Platform-Specific Code + +- macOS: `NoNap.mm`, `vsMacPermissions.mm`, CoreAudio integration, DYLD injection protection +- Windows: RtAudio primary backend, DLL management in `win/` +- Linux: Strong JACK support, Flatpak packaging in `linux/` + +## Adding Icons to the UI + +Prefer sourcing icons from [heroicons.com](https://heroicons.com). Download the SVG and follow the steps below. + +### Steps + +1. **Add the SVG file** to `src/vs/` (or `src/vs/flags/` for country flags) +2. **Register it in `src/vs/vs.qrc`** — add a `youricon.svg` entry inside the `` block +3. **Reference in QML** — use `icon.source: "youricon.svg"` or the `AppIcon` wrapper component for theme-aware rendering: + ```qml + AppIcon { + width: 24 * virtualstudio.uiScale + height: 24 * virtualstudio.uiScale + icon.source: "youricon.svg" + color: textColour + } + ``` +4. **Rebuild** — Meson recompiles the QRC, embedding the icon into the executable + +### QRC files + +- `src/vs/vs.qrc` — Virtual Studio icons, QML files, fonts (prefix: `vs`) +- `src/images/images.qrc` — application window icons (prefix: `images`) + +### Icon conventions + +- **Format**: SVG (all UI icons are SVG; PNG only for app icons and branding) +- **Color**: icons are colored at runtime via `icon.color` in QML, so use a single fill color in the SVG (the `AppIcon` component in `src/vs/AppIcon.qml` handles dark/light theme defaults automatically) +- **Sizing**: controlled by the parent QML element and `virtualstudio.uiScale`, not baked into the SVG + +## Code Style Conventions + +- **Classes**: PascalCase (`JackTrip`, `AudioInterface`) +- **Methods**: camelCase (`startProcess`, `computeProcessFromNetwork`) +- **Member variables**: `m` prefix (`mAudioInterface`, `mDataProtocol`) +- Heavy use of Qt signals/slots, `QObject`, `Q_PROPERTY` +- Pointers: left-aligned (`int* ptr`, not `int *ptr`) + +## Cursor Cloud specific instructions + +### Building in the cloud VM + +The default C compiler on the VM is clang, which fails meson's C++ compiler check due to missing libstdc++ headers. Always set `CC=gcc CXX=g++` when running `meson setup`: + +```bash +CC=gcc CXX=g++ meson setup -Dnogui=true -Drtaudio=enabled \ + -Drtaudio:jack=disabled -Drtaudio:default_library=static \ + -Drtaudio:alsa=enabled -Drtaudio:pulse=disabled -Drtaudio:werror=false \ + -Dnofeedback=true -Dlibsamplerate=enabled -Ddefault_library=shared builddir + +meson compile -C builddir +``` + +- Use `-Dnogui=true` for headless/CLI builds (avoids X11/GUI dependencies). +- Use `-Dnovs=true` instead if you want the classic GUI but not Virtual Studio (requires additional Qt6 GUI/Widgets packages). +- Git submodules must be initialized: `git submodule update --init --recursive`. + +### Running the hub server + +The hub server **requires a running JACK daemon**. Start JACK with a dummy audio driver first: + +```bash +jackd -d dummy -r 48000 -p 1024 & +``` + +Then start the hub server: `./builddir/jacktrip -S` + +To test a client connecting to the local hub server on the same host, use separate bind and peer ports (the server listens on TCP 4464, so the client must keep peer port at 4464 but use a different bind port): + ```bash + ./builddir/jacktrip -C 127.0.0.1 -B 4465 -P 4464 + ``` + Note: `-o` offsets **both** bind and peer ports, which breaks same-host testing since the client would try to connect to the wrong TCP port. + +Use `jack_lsp` to verify JACK ports are registered after a client connects. + +### Linting + +The CI runs clang-format version 13; the cloud VM has a newer version which may flag pre-existing style differences. Run on changed files only to match CI behavior: + +```bash +clang-format --dry-run --Werror src/YourFile.cpp +``` + +### Gotchas + +- `meson setup` downloads RtAudio as a subproject automatically if not found on the system; this requires network access. +- Rebuilding after source changes: `meson compile -C builddir` (incremental). +- To reconfigure: `meson configure builddir -Doption=value` or wipe with `rm -rf builddir`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/Dockerfile b/Dockerfile index 3a6d3df..07e2371 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ ARG JACK_VERSION=latest FROM registry.fedoraproject.org/fedora:${FEDORA_VERSION} AS builder # install tools require to build jacktrip -RUN dnf install -y --nodocs cmake gcc gcc-c++ meson git python3-pyyaml python3-jinja2 glib2-devel jack-audio-connection-kit-devel dbus-devel +RUN dnf install -y --nodocs cmake gcc gcc-c++ meson git perl python3-pyyaml python3-jinja2 glib2-devel jack-audio-connection-kit-devel dbus-devel libatomic-static ENV QT_VERSION=6.8.3 RUN if [ "$(uname -m)" = "x86_64" ]; then export ARCH=amd64; else export ARCH=arm64; fi \ @@ -33,7 +33,7 @@ RUN cd /root \ && export QT_PATH=/opt/qt-${QT_VERSION}-static \ && export PATH=${QT_PATH}/bin:${PATH} \ && export LDFLAGS="-L${QT_PATH}/lib -L${QT_PATH}/plugins/tls" \ - && meson setup -Ddefault_library=static -Dnogui=true --buildtype release builddir \ + && meson setup -Dpkg_config_path=/opt/qt-${QT_VERSION}-static/lib/pkgconfig -Dlibdatachannel=enabled -Dmsquic=enabled -Ddefault_library=static -Dnogui=true --buildtype release builddir \ && meson compile -C builddir # stage files in INSTALLDIR @@ -65,4 +65,5 @@ COPY --from=builder /artifacts / # jacktrip hub server listens on 4464 and uses 61000+ for clients EXPOSE 4464/tcp +EXPOSE 4464/udp EXPOSE 61000-61100/udp diff --git a/docs/Build/Linux.md b/docs/Build/Linux.md index 144624f..87e36a8 100644 --- a/docs/Build/Linux.md +++ b/docs/Build/Linux.md @@ -43,7 +43,7 @@ apt install qtbase5-dev qtbase5-dev-tools qtchooser qt5-qmake qttools5-dev libqt ### Ubuntu and Debian/Raspbian (Qt6) ```sh apt install --no-install-recommends build-essential autoconf automake libtool make libjack-jackd2-dev git help2man libclang-dev libdbus-1-dev libdbus-1-dev python3-jinja2 -apt install -y libqt6core6 libqt6gui6 libqt6network6 libqt6widgets6 libqt6qml6 libqt6qmlcore6 libqt6quick6 libqt6quickcontrols2-6 libqt6svg6 libqt6webchannel6 libqt6webengine6-data libqt6webenginecore6 libqt6webenginecore6-bin libqt6webenginequick6 libqt6websockets6 libqt6shadertools6 qt6-qpa-plugins qml6-module-qtquick-controls qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qt5compat-graphicaleffects qml6-module-qtwebchannel qml6-module-qtwebengine qml6-module-qtquick-window +apt install -y libqt6core6 libqt6gui6 libqt6network6 libqt6widgets6 libqt6qml6 libqt6qmlcore6 libqt6quick6 libqt6quickcontrols2-6 libqt6svg6 libqt6webchannel6 libqt6webengine6-data libqt6webenginecore6 libqt6webenginecore6-bin libqt6webenginequick6 libqt6websockets6 libqt6shadertools6 qt6-qpa-plugins qml6-module-qtquick-controls qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qt5compat-graphicaleffects qml6-module-qtwebchannel qml6-module-qtwebengine qml6-module-qtquick-window qml6-module-qtquick-dialogs apt install qt6-base-dev qt6-base-dev-tools qmake6 qt6-tools-dev qt6-declarative-dev qt6-webengine-dev qt6-webview-dev qt6-webview-plugins libqt6svg6-dev libqt6websockets6-dev libqt6core5compat6-dev libqt6shadertools6-dev libgl1-mesa-dev # for GUI builds apt install libfreetype6-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev libx11-xcb-dev libdrm-dev libglu1-mesa-dev libwayland-dev libwayland-egl1-mesa libgles2-mesa-dev libwayland-server0 libwayland-egl-backend-dev libxcb1-dev libxext-dev libfontconfig1-dev libxrender-dev libxcb-keysyms1-dev libxcb-image0-dev libxcb-shm0-dev libxcb-icccm4-dev '^libxcb.*-dev' libxcb-render-util0-dev libxcomposite-dev libgtk-3-dev diff --git a/docs/Documentation/ClientConnectionTypes.md b/docs/Documentation/ClientConnectionTypes.md new file mode 100644 index 0000000..349edd2 --- /dev/null +++ b/docs/Documentation/ClientConnectionTypes.md @@ -0,0 +1,387 @@ +# JackTrip Hub Server Connection Types + +## Overview + +The JackTrip Hub Server supports three different connection types for clients, each with different characteristics suited to different deployment scenarios: + +1. **UDP** - Traditional low-latency UDP transport with TCP signaling +2. **WebRTC** - Browser-compatible connection with NAT traversal using WebRTC data channels +3. **WebTransport** - Modern HTTP/3 transport using QUIC with built-in encryption + +All three connection types use the same audio packet format (see [NetworkProtocol.md](NetworkProtocol.md)) and share the same worker pool allocation mechanism. + +## Connection Type Comparison + +| Feature | UDP | WebRTC | WebTransport | +|---------|-----|--------|--------------| +| **Transport** | UDP datagrams | WebRTC data channels over UDP | QUIC datagrams over UDP | +| **Signaling** | TCP port 4464 | WebSocket over TCP 4464 | HTTP/3 over UDP 4464 | +| **NAT Traversal** | No | Yes (ICE/STUN/TURN) | Yes (QUIC connection migration) | +| **Browser Support** | No | Yes (all modern browsers) | Yes (Chrome 97+, Edge 97+) | +| **Encryption** | Optional (TLS) | Mandatory (DTLS) | Mandatory (TLS 1.3) | +| **Setup Complexity** | Simple | Complex (ICE negotiation) | Medium (HTTP/3 CONNECT) | +| **Connection Time** | Fastest | Medium (ICE gathering) | Fast (0-RTT after first) | +| **Audio Transport Port** | UDP (61002 + worker_id) | ICE-negotiated UDP ports | UDP 4464 (QUIC) | +| **Library Required** | None (Qt Network) | libdatachannel | msquic | +| **Build Option** | Always available | `-Dlibdatachannel=enabled` | `-Dmsquic=enabled` | + +## 1. UDP Connections + +### Overview + +Traditional UDP connections use a simple TCP-based signaling handshake followed by direct UDP audio transport. This is the lowest-latency option but requires open firewall ports and doesn't work behind NAT without port forwarding. + +### Connection Handshake + +1. Client connects to TCP port 4464 +2. Client sends UDP port number (4 bytes, little-endian) and optional client name (64 bytes) +3. Server allocates a worker slot and responds with server UDP port (`mBasePort + worker_id`) +4. TCP connection closes +5. Audio exchange begins over UDP using the negotiated ports + +For complete details on the UDP handshake protocol, packet format, and authentication flow, see [NetworkProtocol.md](NetworkProtocol.md). + +### Port Requirements + +- **TCP 4464**: Signaling handshake +- **UDP 61002 + worker_id**: Audio transport (base port configurable with `--udpbaseport`) + - Example: First client uses 61002, second uses 61003, etc. + +### Authentication + +Optional TLS authentication is supported. When enabled: +- Client sends special value (`65536`) instead of port to initiate SSL handshake +- Credentials (username/password) are exchanged over encrypted connection +- Server validates and responds with port assignment or error code + +## 2. WebRTC Connections + +### Overview + +WebRTC connections use WebSocket-based signaling followed by ICE-negotiated data channels. This provides excellent NAT traversal and works from web browsers, making it ideal for browser-based clients. + +### Connection Handshake + +1. **WebSocket Upgrade**: Client sends HTTP upgrade request to TCP port 4464 + ```http + GET / HTTP/1.1 + Upgrade: websocket + Connection: Upgrade + Sec-WebSocket-Key: + ``` + +2. **SDP Exchange**: Client sends SDP offer, server responds with answer + ```json + { + "type": "offer", + "sdp": "v=0\r\no=- 4611731400430051336 2 IN IP4 127.0.0.1\r\n..." + } + ``` + +3. **ICE Candidates**: Both sides exchange ICE candidates for connectivity + ```json + { + "type": "ice", + "candidate": "candidate:1 1 UDP 2130706431 192.168.1.100 54321 typ host", + "sdpMid": "data", + "sdpMLineIndex": 0 + } + ``` + +4. **Connection Establishment**: ICE performs connectivity checks, DTLS establishes encryption, SCTP creates data channel association + +5. **Audio Exchange**: Audio packets are sent over the data channel using the same packet format as UDP + +### Data Channel Configuration + +The data channel is configured for low-latency, unreliable delivery similar to UDP: + +```cpp +rtc::DataChannelInit config; +config.ordered = false; // Don't wait for in-order delivery +config.maxRetransmits = 0; // No retransmissions (like UDP) +``` + +### ICE Server Configuration + +The server can be configured with STUN/TURN servers for NAT traversal: + +```bash +jacktrip -S --iceservers "stun:stun.l.google.com:19302" +``` + +### Implementation + +- **Library**: libdatachannel +- **Classes**: `WebRtcPeerConnection`, `WebRtcDataProtocol`, `WebRtcSignalingProtocol` +- **Detection**: Server detects WebSocket upgrade by checking for "GET" in initial TCP data + +## 3. WebTransport Connections + +### Overview + +WebTransport provides modern, low-latency transport using HTTP/3 over QUIC. Unlike WebRTC, it requires no ICE negotiation and provides a simpler connection model with built-in NAT traversal. All connections use unreliable QUIC datagrams for audio transport. + +**Important**: WebTransport uses UDP (not TCP) for the entire connection, including signaling. + +### Connection Handshake + +1. **QUIC Connection**: Client initiates QUIC connection to UDP port 4464 + - TLS 1.3 handshake (mandatory, built into QUIC) + - 0-RTT capable after first connection + +2. **HTTP/3 CONNECT**: Client sends HTTP/3 CONNECT request over QUIC + ``` + :method = CONNECT + :protocol = webtransport + :path = /webtransport + :authority = server.example.com:4464 + ``` + +3. **Session Established**: Server responds with 200 OK + ``` + :status = 200 + sec-webtransport-http3-draft = draft02 + ``` + +4. **Audio Exchange**: Audio packets are sent as QUIC DATAGRAM frames (RFC 9221) + +### QUIC Datagram Transport + +Audio packets are sent as unreliable QUIC datagrams: + +``` +┌──────────┬────────────────────┐ +│ JackTrip │ Audio Samples │ +│ Header │ │ +│ (16B) │ (variable) │ +└──────────┴────────────────────┘ +``` + +**Key properties:** +- Unreliable (no retransmissions, like UDP) +- Unordered (can arrive out of sequence) +- Encrypted (TLS 1.3 via QUIC) +- Preserve datagram boundaries +- Typical size limit: 1200 bytes (path MTU) + +### TLS Certificate Requirements + +WebTransport requires TLS 1.3 certificates - encryption is mandatory and cannot be disabled. + +**Development (self-signed):** +```bash +openssl genpkey -algorithm RSA -out webtransport.key -pkeyopt rsa_keygen_bits:2048 +openssl req -new -x509 -key webtransport.key -out webtransport.crt -days 365 \ + -subj "/CN=jacktrip.example.com" + +jacktrip -S --certfile webtransport.crt --keyfile webtransport.key +``` + +**Production (Let's Encrypt):** +```bash +sudo certbot certonly --standalone -d jacktrip.example.com + +jacktrip -S \ + --certfile /etc/letsencrypt/live/jacktrip.example.com/fullchain.pem \ + --keyfile /etc/letsencrypt/live/jacktrip.example.com/privkey.pem +``` + +**Note**: Browsers will reject self-signed certificates unless explicitly trusted. + +### Implementation + +- **Library**: msquic (only supported QUIC library) +- **Classes**: `WebTransportSession`, `WebTransportDataProtocol` +- **Detection**: Server detects QUIC packets on UDP 4464 by examining packet header flags +- **Port**: Single UDP port (4464) for both signaling and audio + +### Why QUIC? + +QUIC provides several advantages over TCP for real-time audio: + +- **True unreliable datagrams**: Native support for unreliable delivery (no head-of-line blocking) +- **Lower latency**: 1-RTT connection setup (0-RTT after first connection) +- **Connection migration**: Survives IP address changes (WiFi ↔ Cellular) +- **Single port operation**: All communication over one UDP port +- **No framing overhead**: QUIC datagrams preserve packet boundaries + +## Connection Type Detection + +The server automatically detects the connection type: + +``` +┌─────────────────────┐ +│ Incoming Connection │ +└──────────┬──────────┘ + │ + ┌──────┴──────┐ + │ │ +TCP 4464 UDP 4464 + │ │ + │ └──> QUIC packet → WebTransport + │ + └──> Peek first bytes + │ + ├──> "GET" → WebRTC (WebSocket) + │ + └──> Binary (4 bytes) → UDP +``` + +### Detection Logic + +**TCP port 4464** (UDP and WebRTC): +```cpp +QByteArray peekData = clientConnection->peek(512); + +if (peekData.startsWith("GET")) { + // WebRTC connection (WebSocket signaling) + createWebRtcWorker(clientConnection, "webrtc"); +} else { + // Binary data - legacy UDP client + readClientUdpPort(clientConnection, clientName); +} +``` + +**UDP port 4464** (WebTransport): +```cpp +// QUIC packets have distinctive header format +uint8_t first_byte = datagram[0]; +bool is_long_header = (first_byte & 0x80) != 0; + +if (is_long_header) { + // QUIC Initial or Handshake packet + handleQuicConnection(datagram, sender, senderPort); +} +``` + +## Audio Packet Format + +All three connection types use the same audio packet format. See [NetworkProtocol.md](NetworkProtocol.md) for complete details on: + +- Packet header structure (16 bytes) +- Audio payload format (planar/non-interleaved) +- Sample encoding (8/16/24/32-bit) +- Special field encodings + +## Worker Pool Management + +All connection types share the same worker pool: + +- **Slot allocation**: First available slot from 0 to `gMaxThreads-1` +- **Audio ports**: All create identical JACK/RtAudio ports (`receive_N`, `send_N` where N = worker_id + 1) +- **Port assignment for UDP**: Each UDP client is assigned `mBasePort + worker_id` (typically 61002 + worker_id) for audio transport + +## Building with Connection Type Support + +### WebRTC Support + +```bash +# Auto-detect libdatachannel (default) +meson setup build + +# Explicitly enable (error if not available) +meson setup build -Dlibdatachannel=enabled + +# Explicitly disable +meson setup build -Dlibdatachannel=disabled +``` + +When enabled, defines `WEBRTC_SUPPORT` macro. + +### WebTransport Support + +```bash +# Auto-detect msquic (default) +meson setup build + +# Explicitly enable (error if not available) +meson setup build -Dmsquic=enabled + +# Explicitly disable +meson setup build -Dmsquic=disabled +``` + +When enabled, defines `WEBTRANSPORT_SUPPORT` macro. + +## Server Configuration + +### Starting the Server + +```bash +# Start hub server (binds to both TCP and UDP port 4464) +jacktrip -S + +# Specify custom server port +jacktrip -S -p 4464 + +# Specify custom UDP base port for legacy UDP audio +jacktrip -S --udpbaseport 61002 + +# Configure ICE servers for WebRTC +jacktrip -S --iceservers "stun:stun.l.google.com:19302" + +# Enable TLS for WebTransport (and optionally UDP auth) +jacktrip -S --certfile server.crt --keyfile server.key +``` + +### Firewall Configuration + +```bash +# TCP for UDP and WebRTC signaling +sudo ufw allow 4464/tcp + +# UDP for WebTransport and legacy audio +sudo ufw allow 4464/udp + +# UDP port range for legacy UDP audio streams +sudo ufw allow 61002:62000/udp +``` + +### Port Summary + +| Connection Type | Port | Protocol | Purpose | +|----------------|------|----------|---------| +| UDP | 4464 | TCP | Signaling handshake | +| UDP | 61002 + worker_id | UDP | Audio transport | +| WebRTC | 4464 | TCP | WebSocket signaling | +| WebRTC | ICE-negotiated | UDP | Audio transport (data channels) | +| WebTransport | 4464 | UDP | QUIC (signaling + audio) | + +## Error Handling + +### UDP Errors + +- **Port already bound**: Worker slot exhausted or port conflict +- **Authentication failed**: Invalid credentials (if auth enabled) +- **Timeout**: Client doesn't send UDP packets after handshake + +### WebRTC Errors + +- **ICE failed**: No connectivity path found +- **DTLS handshake failed**: Certificate or crypto mismatch +- **Data channel failed**: SCTP association error + +### WebTransport Errors + +- **Handshake failed**: Invalid HTTP/3 CONNECT request +- **Certificate verification failed**: Invalid or untrusted TLS certificate +- **Session closed**: QUIC connection terminated + +## References + +### Source Files + +- **UDP**: `src/UdpHubListener.cpp`, `src/UdpDataProtocol.cpp` +- **WebRTC**: `src/webrtc/WebRtcPeerConnection.cpp`, `src/webrtc/WebRtcDataProtocol.cpp`, `src/webrtc/WebRtcSignalingProtocol.cpp` +- **WebTransport**: `src/webtransport/WebTransportSession.cpp`, `src/webtransport/WebTransportDataProtocol.cpp` +- **Worker**: `src/JackTripWorker.cpp` + +### External Documentation + +- [NetworkProtocol.md](NetworkProtocol.md) - Detailed packet format and UDP protocol +- [WebRTC Specification](https://www.w3.org/TR/webrtc/) +- [WebTransport Specification](https://www.w3.org/TR/webtransport/) +- [RFC 9221 - QUIC Datagrams](https://www.rfc-editor.org/rfc/rfc9221.html) +- [libdatachannel](https://github.com/paullouisageneau/libdatachannel) +- [MsQuic](https://github.com/microsoft/msquic) diff --git a/docs/Documentation/NetworkProtocol.md b/docs/Documentation/NetworkProtocol.md new file mode 100644 index 0000000..5ec2fdb --- /dev/null +++ b/docs/Documentation/NetworkProtocol.md @@ -0,0 +1,370 @@ +## JackTrip network protocol (as implemented) + +This document describes JackTrip’s **on-the-wire protocol** as implemented in the current source tree. It is intended for developers debugging or interoperating with JackTrip at the packet level. + +### Scope and non-goals + +- **In scope**: the real-time **UDP audio stream**, its headers and payload layout, the optional **UDP redundancy** framing, the small **UDP “stop” control packet**, the **TCP handshake** used by hub/ping-server style deployments (including the authentication variant), the **WebRTC data channel transport** (used by the hub server’s WebRTC path), and the **WebTransport transport** (HTTP/3 over QUIC datagrams). +- **Out of scope**: local-only IPC (e.g. `QLocalSocket` “AudioSocket”), OSC control, and any higher-level application semantics outside packet exchange. + +### Transports at a glance + +- **UDP (audio)**: real-time audio is sent as UDP datagrams containing `PacketHeader` + raw audio payload. +- **UDP (control)**: a small fixed-size “stop” datagram is used to signal shutdown. +- **TCP (hub/ping-server handshake)**: a short-lived TCP connection is used to exchange ephemeral UDP port information (and optionally do TLS + credentials). The client sends 4 bytes representing the port number it is binding to, and the server responds by sending 4 bytes representing its own port number. +- **WebRTC data channel (audio)**: JackTrip hub server’s WebRTC path uses a WebRTC data channel to carry the same packet format (header + planar audio payload) as the UDP stream. Signaling uses an encrypted WebSocket (`wss://`) on the hub TCP port; plain `ws://` is not accepted. The same interleaving conversion applies. See `WebRtcDataProtocol.cpp`. +- **WebTransport / QUIC datagrams (audio)**: the hub server’s WebTransport path uses HTTP/3 over QUIC (via msquic) with unreliable QUIC datagrams (RFC 9221) to carry the same packet format as the UDP stream. The WebTransport session is established with an HTTP/3 CONNECT request before audio flows. See `src/webtransport/` and `src/http3/`. + +--- + +## UDP audio datagrams + +### High-level framing + +Each UDP datagram carries one of: + +- **Audio datagram**: one or more **full packets** (header + audio payload). When redundancy is disabled, there is exactly one full packet per UDP datagram. When redundancy is enabled, multiple full packets are concatenated into a single UDP datagram to provide forward error correction (FEC) (see “UDP redundancy”). +- **Stop/control datagram**: exactly 63 bytes of `0xFF` (see “UDP stop/control datagram”). + +### Packet header types + +The header is selected by `DataProtocol::packetHeaderTypeT`: + +- **DEFAULT**: `DefaultHeaderStruct` (the standard JackTrip header). +- **JAMLINK**: `JamLinkHeaderStuct` (JamLink compatibility). +- **EMPTY**: no header (payload only). + +See `src/PacketHeader.h` and `src/PacketHeader.cpp`. + +### Default header (`DEFAULT`) + +On-wire layout is the in-memory `DefaultHeaderStruct` copied with `memcpy()` (no explicit endian conversions). + +Fields (in order): + +| Field | Type | Meaning | +|------:|------|---------| +| `TimeStamp` | `uint64_t` | Timestamp in microseconds since Unix epoch (see `PacketHeader::usecTime()`). | +| `SeqNumber` | `uint16_t` | Sequence number; increments once per audio period and wraps at 16 bits. | +| `BufferSize` | `uint16_t` | Audio period size \(N\) in **samples per channel**. | +| `SamplingRate` | `uint8_t` | Encoded sample-rate enum value (`AudioInterface::samplingRateT`), **not** Hz. | +| `BitResolution` | `uint8_t` | Bits per sample (8/16/24/32). | +| `NumIncomingChannelsFromNet` | `uint8_t` | Channel count expected from the peer “from network” direction (see notes below). | +| `NumOutgoingChannelsToNet` | `uint8_t` | Channel count the sender is placing into the payload (see notes below). | + +#### Important interoperability notes + +- **Endianness / ABI**: this header is serialized by raw `memcpy()` of a C struct. In practice this assumes: + - both sides are using compatible ABI/layout for the struct, and + - both sides are on the same endianness (typically **little-endian** on modern desktop platforms). +- **Channel fields are asymmetric**: the implementation uses these fields to convey “incoming vs outgoing” channel counts, including a couple of sentinel behaviors: + - `NumIncomingChannelsFromNet` is populated from local *audio interface output* channel count. + - `NumOutgoingChannelsToNet` may be set to `0` when in/out channel counts match, or to `0xFF` when there are zero audio interface input channels. + +These behaviors come from `DefaultHeader::fillHeaderCommonFromAudio()` in `src/PacketHeader.cpp`. + +### JamLink header (`JAMLINK`) + +Please note that JamLink is an obsolete device. + +JamLink uses a compact header: + +| Field | Type | Meaning | +|------:|------|---------| +| `Common` | `uint16_t` | Bitfield describing mono/stereo, bit depth, sample rate, and samples-per-packet (JamLink “streamType”). | +| `SeqNumber` | `uint16_t` | Sequence number. | +| `TimeStamp` | `uint32_t` | Timestamp. | + +The current implementation primarily fills this for JamLink constraints (mono, 48kHz, 64-sample buffers). See `JamLinkHeader::fillHeaderCommonFromAudio()` in `src/PacketHeader.cpp`. + +### Empty header (`EMPTY`) + +No header; the UDP payload is raw audio data only. + +--- + +## UDP audio payload + +### Size + +For a single full packet (no redundancy), the UDP payload length is: + +$$\text{headerBytes} + (N \times C \times \text{bytesPerSample})$$ + +Where: + +- \(N\) is `BufferSize` (samples per channel) +- \(C\) is the number of channels present in the payload +- `bytesPerSample` is `BitResolution / 8` + +### Channel/sample ordering (planar / non-interleaved) + +On the wire, the payload is **planar** (non-interleaved) by channel: + +- First \(N\) samples for channel 0 +- Then \(N\) samples for channel 1 +- … + +This is explicit in `UdpDataProtocol` which converts between: + +- **Internal**: interleaved layout \([n][c]\) +- **Network**: planar layout \([c][n]\) + +For **mono** (\(C = 1\)) there is no difference between planar and interleaved layouts, so no conversion is needed. Multi-channel conversion also applies on the WebRTC data channel path (see `WebRtcDataProtocol.cpp`) and the WebTransport path. + +See `UdpDataProtocol::sendPacketRedundancy()` and `UdpDataProtocol::receivePacketRedundancy()` in `src/UdpDataProtocol.cpp`. + +### Sample encoding (bit resolution) + +JackTrip processes audio internally as `float` (`sample_t`), but the network payload uses the selected bit resolution via `AudioInterface::fromSampleToBitConversion()` / `fromBitToSampleConversion()`. + +Behavior by bit resolution (`AudioInterface::audioBitResolutionT`): + +- **8-bit (`BIT8`)**: signed 8-bit integer, scaled from float in \([-1, 1]\). +- **16-bit (`BIT16`)**: signed 16-bit integer, written **little-endian**. +- **24-bit (`BIT24`)**: a **non-standard 3-byte format**: a 16-bit signed integer plus an 8-bit unsigned “remainder” byte. +- **32-bit (`BIT32`)**: raw 32-bit float bytes (`memcpy` of `float`), which implicitly assumes IEEE-754 and matching endianness. + +See `src/AudioInterface.cpp`. + +--- + +## UDP redundancy (optional) + +JackTrip can send redundant audio packets to reduce audible artifacts from packet loss. + +### Framing + +With redundancy factor \(R\), each UDP datagram contains **R full packets** concatenated: + +- The newest packet is first (`UDP[n]`), followed by older packets (`UDP[n-1]`, …). +- Total UDP payload length becomes `R * full_packet_size`. + +The sender implements this by shifting a buffer and prepending the newest full packet each period. + +See `UdpDataProtocol::sendPacketRedundancy()` and the explanatory comment block in `src/UdpDataProtocol.cpp`. + +### Receiver behavior + +Upon receiving a redundant datagram, the receiver: + +- Reads the first packet’s `SeqNumber`. +- If it is not the next expected sequence, scans forward through the concatenated packets looking for the expected next one. +- May “revive” and deliver multiple packets from the redundant datagram in order. +- Treats large negative or implausibly large sequence jumps as **out-of-order** and ignores them. + +See `UdpDataProtocol::receivePacketRedundancy()` in `src/UdpDataProtocol.cpp`. + +--- + +## UDP stop/control datagram + +JackTrip uses a special fixed-size UDP datagram to signal shutdown: + +- **Length**: 63 bytes +- **Contents**: every byte is `0xFF` + +The receiver checks for this exact pattern and treats it as “Peer Stopped”. + +See `UdpDataProtocol::processControlPacket()` and the shutdown path in `UdpDataProtocol::run()` in `src/UdpDataProtocol.cpp`. + +--- + +## Connection setup and “handshake” + +JackTrip supports multiple deployment styles. The relevant “protocol” differs depending on mode. + +### P2P server mode (UDP-only) + +In P2P server mode, there is **no TCP handshake**. Instead: + +- The server binds a UDP socket on its configured receive port. +- It waits for the first UDP datagram. +- It uses the datagram’s source address/port as the peer endpoint for subsequent UDP send/receive. + +This supports basic NAT traversal by responding to the client’s observed source port. + +See `JackTrip::serverStart()` and `JackTrip::receivedDataUDP()` in `src/JackTrip.cpp`. + +### Hub / ping-server mode (TCP handshake + UDP audio) + +When connecting to a hub/ping-server style endpoint, JackTrip uses a short-lived TCP connection to exchange UDP port information. + +#### Unauthenticated handshake (no TLS) + +Client → server (TCP): + +- `int32` little-endian: the client’s UDP receive/bind port +- `gMaxRemoteNameLength` bytes: optional UTF-8 “remote client name” (null-terminated, padded with zeros) + +Server → client (TCP): + +- `int32` little-endian: the server-assigned UDP port the client should use as its peer port + +The TCP connection is then closed. + +Client-side send/receive logic: `JackTrip::receivedConnectionTCP()` and `JackTrip::receivedDataTCP()` in `src/JackTrip.cpp` +Server-side receive/send logic: `UdpHubListener::readClientUdpPort()` and `UdpHubListener::sendUdpPort()` in `src/UdpHubListener.cpp` + +#### Authentication / TLS handshake (optional) + +This is an extension of the same TCP handshake using values above 65535 as “auth response” codes. + +High-level flow: + +1. Client connects TCP and sends an `int32` little-endian value of `Auth::OK` to request authentication. +2. Server replies with an `int32` auth response (e.g. `Auth::OK`, `Auth::NOTREQUIRED`, `Auth::REQUIRED`, …). +3. If both sides proceed, TLS is established on the same TCP socket. +4. Client then sends: + - `int32` LE: UDP receive/bind port + - `gMaxRemoteNameLength` bytes: client name + - `int32` LE: username length (excluding null terminator) + - `int32` LE: password length (excluding null terminator) + - `username` bytes + `\0` + - `password` bytes + `\0` +5. Server validates credentials and replies with either: + - `int32` LE UDP port (<= 65535) on success, or + - `int32` LE auth error code (> 65535) on failure + +Client-side: `JackTrip::receivedConnectionTCP()`, `JackTrip::connectionSecured()`, and `JackTrip::receivedDataTCP()` in `src/JackTrip.cpp` +Server-side: `UdpHubListener::receivedClientInfo()`, `UdpHubListener::checkAuthAndReadPort()`, and `UdpHubListener::sendUdpPort()` in `src/UdpHubListener.cpp` + +--- + +## QoS marking (best-effort) + +On supported platforms, JackTrip attempts to mark UDP packets as “voice” traffic: + +- Linux/Unix: sets DSCP to 56 (`IP_TOS` / `IPV6_TCLASS` set to `0xE0`), and sets `SO_PRIORITY` to 6. +- Windows: uses QOS APIs with `QOSTrafficTypeVoice`. +- macOS: uses `SO_NET_SERVICE_TYPE` with `NET_SERVICE_TYPE_VO` (best-effort). + +See `src/UdpDataProtocol.cpp`. + +--- + +## WebRTC data channel transport + +JackTrip's WebRTC path carries the same audio packet format as the UDP stream but over a WebRTC data channel. It enables NAT traversal through ICE and is implemented in `src/webrtc/` using the libdatachannel library. + +### Why an unordered, unreliable data channel + +The data channel is configured for **unordered, unreliable** delivery — equivalent to UDP semantics — to minimise latency. Retransmissions and head-of-line blocking are explicitly disabled. + +### Transport requirement: encrypted WebSocket (WSS) + +WebRTC clients **must** connect using an encrypted WebSocket (`wss://`). Plain-text WebSocket (`ws://`) is not accepted. This requirement exists because browsers only allow `wss://` from HTTPS pages, and because the TLS layer is what allows the server to multiplex WebRTC and legacy binary clients on the same port (see "Protocol detection" below). + +The server must be started with `--certfile` and `--keyfile` for WebRTC connections to succeed. Without a loaded TLS certificate, any TLS ClientHello is rejected with a logged error and the connection is closed. + +### Protocol detection on the hub TCP port + +The hub server multiplexes three connection types on a single TCP listen port. The server inspects the first three bytes of each new connection to route it correctly: + +| First 3 bytes (hex) | Interpretation | +| ------------------------------- | --------------------------------------------------------------------------------- | +| `16 03 01` through `16 03 04` | TLS ClientHello (browser `wss://`) — start TLS handshake; re-detect after decrypt | +| Anything else | Legacy binary hub protocol — read 32-bit LE port number | + +After the TLS handshake completes the server inspects the first decrypted bytes: + +| Decrypted content | Interpretation | +| ------------------------------ | ------------------------------------------------------------ | +| `GET /ping …` | Health-check endpoint — responds `{"status":"OK"}` and closes | +| `GET /webrtc …` | HTTP WebSocket upgrade → WebRTC signaling path | +| Other `GET …` | Unsupported path — responds HTTP 404 and closes | +| Other binary data | Authenticated binary hub protocol (credentials follow) | + +**Why 3 bytes are needed for unambiguous TLS detection** + +The binary protocol sends a 32-bit little-endian port number (≤ 65535) as its first 4 bytes, which means bytes 2 and 3 are always `0x00`. TLS record headers always have byte 2 set to `0x01`–`0x04` (the TLS minor version). Checking only the first byte would produce false positives: port 22 (`{0x16, 0x00, …}`) and port 790 (`{0x16, 0x03, 0x00, …}`) both start with byte sequences that overlap with TLS. Requiring all three bytes `{0x16, 0x03, 0x01–0x04}` is provably collision-free with any valid port number. + +See `UdpHubListener::readyRead()` in `src/UdpHubListener.cpp`. + +### Health-check endpoint (`GET /ping`) + +The hub server exposes a simple health-check endpoint on the same TLS port as WebRTC signaling. It is useful for diagnosing TLS and HTTP connectivity issues before attempting a WebSocket upgrade. + +**Request:** +``` +GET /ping HTTP/1.1 +``` + +**Response:** +``` +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 15 +Connection: close + +{"status":"OK"} +``` + +The connection is closed immediately after the response is sent. The endpoint is only available when the binary is built with `WEBRTC_SUPPORT` and when TLS is configured (`--certfile` / `--keyfile`). A plain `curl` command can verify connectivity: + +```bash +curl -k https://:/ping +``` + +### Signaling message framing + +All signaling messages are JSON objects framed with a **4-byte big-endian length prefix** over the TCP socket: + +``` +[4-byte length (BE)] [JSON payload] +``` + +### Signaling flow + +1. Client opens a TLS connection to the hub TCP port (`wss://`). The server detects the TLS ClientHello by its 3-byte record header and performs the TLS handshake. +2. Client sends an HTTP `GET /webrtc` request with `Upgrade: websocket` headers. The server upgrades the connection to a WebSocket. +3. Client sends `PROTOCOL_DETECT` message: `{"type": "protocol_detect", "protocol": 2, "clientName": "…", "version": 1}`. +4. Server responds with its own `PROTOCOL_DETECT` acknowledgement. +5. Client sends an `OFFER` message containing its SDP. +6. Server sets the remote description, generates an answer, and sends an `ANSWER` message. +7. Both sides exchange `ICE_CANDIDATE` messages as ICE candidates are gathered. +8. ICE + DTLS handshake completes; the data channel (label `"audio"`) opens. +9. Audio datagrams flow bidirectionally over the data channel. + +Either side can send a `HANGUP` message to terminate the session. + +### Packet format + +Identical to UDP: the standard JackTrip packet header followed by the planar audio payload. The same non-interleaved channel layout and interleaving conversion apply. No additional framing is added inside the data channel message. + +See `src/webrtc/WebRtcDataProtocol.cpp`, `src/webrtc/WebRtcPeerConnection.cpp`, and `src/webrtc/WebRtcSignalingProtocol.cpp`. + +--- + +## WebTransport transport (HTTP/3 / QUIC datagrams) + +JackTrip's WebTransport path carries the same audio packet format as the UDP stream but over HTTP/3 using unreliable QUIC datagrams (RFC 9221). It is implemented in `src/webtransport/` and `src/http3/`, using Microsoft's msquic library. + +### Why QUIC datagrams + +QUIC datagrams provide UDP-like unreliable, unordered delivery without head-of-line blocking, which makes them well suited for low-latency audio. The QUIC transport layer still handles path MTU discovery and congestion signalling. + +### Connection setup + +1. The server starts an `Http3Server` (backed by msquic) listening on a configured UDP port with a TLS certificate. +2. The client opens a QUIC connection and performs a TLS handshake. +3. Both sides exchange HTTP/3 `SETTINGS` frames on their respective control streams to advertise WebTransport support and datagram receipt capability. +4. The client sends an HTTP/3 `CONNECT` request with `:protocol: webtransport` and a path such as `/webtransport?name=MyClient`. The optional `name` query parameter identifies the client (equivalent to the remote name in the TCP handshake). +5. The server replies with HTTP/3 status `200`, accepting the session. +6. Audio datagrams flow bidirectionally once the session is accepted. + +Each QUIC datagram payload is prefixed with a **quarter stream ID** (a QUIC varint encoding of `stream_id / 4`) per the WebTransport-over-HTTP/3 framing spec, followed immediately by the standard JackTrip packet (header + planar audio payload). Receivers strip the quarter stream ID prefix before processing. + +### Packet format + +Identical to UDP: `DefaultHeaderStruct` (or the selected header type) followed by the planar audio payload. Redundancy and all other payload conventions apply unchanged. + +See `src/webtransport/WebTransportSession.cpp`, `src/http3/Http3Server.cpp`, and `src/http3/Http3Protocol.cpp`. + +--- + +## References + +For additional context on JackTrip's network behavior and interpretation of debug output (`-V` flag): + +Chafe, C. (2018). I am Streaming in a Room. *Frontiers in Digital Humanities*, Volume 5. https://doi.org/10.3389/fdigh.2018.00027 \ No newline at end of file diff --git a/docs/changelog.yml b/docs/changelog.yml index 1bf0351..465111a 100644 --- a/docs/changelog.yml +++ b/docs/changelog.yml @@ -1,3 +1,41 @@ +- Version: "3.0.1" + Date: 2026-08-30 + Description: + - (added) --logtofile option now required for file logging + - (added) Debug builds for Linux amd64 platforms + - (fixed) Crash caused by writing simultaneous log events + - (fixed) Crash caused by JACK buffer size changes + - (fixed) TCP Socket Error when connecting to a studio + - (fixed) PLC sync issues caused by out of order packets + - (fixed) Wrong-slot cleanup for WebRTC/WebTransport workers + - (fixed) Fix for pre-installed msquic linking on Linux +- Version: "3.0.0" + Date: 2026-04-25 + Description: + - (added) VS Mode now uses jacktrip.com for studios list + - (added) Hub server support for WebTransport connections + - (added) Hub server support for WebRTC datachannel connections + - (updated) Default OSC port is now hub server port + 1 + - (updated) Re-enabling classic mode for signed Windows builds + - (updated) VS Mode prefer default audio devices on first run + - (updated) VS Mode restore previously used window dimensions + - (fixed) Potential hub server crash if invalid peer settings + - (fixed) Use DSCP value of 46 instead of 56 for UDP packets + - (fixed) Added guards to protect against buffer overflows + - (fixed) Deep links were sometimes being ignored on Windows + - (fixed) Sending deep links caused segmentation fault on OSX + - (fixed) Stats timer lifecycle issues +- Version: "2.7.2" + Date: 2026-02-06 + Description: + - (added) Documentation for the JackTrip network protocol + - (added) Hub server - log client name with UDP port + - (fixed) Fixed crash when JACK ran out of available ports + - (fixed) Refuse to run if DYLD_INSERT_LIBRARIES is set on OSX + - (fixed) Various PLC quality improvements and bug fixes + - (fixed) Improved error message when studio connection is lost + - (fixed) Suppressed verbose logging of OSC get requests + - (fixed) Added some missing Qt dependencies to Linux docs - Version: "2.7.1" Date: 2025-06-30 Description: diff --git a/linux/Dockerfile.build b/linux/Dockerfile.build index 307b143..a933553 100644 --- a/linux/Dockerfile.build +++ b/linux/Dockerfile.build @@ -15,6 +15,11 @@ FROM ${BUILD_CONTAINER} AS builder # install required packages ENV DEBIAN_FRONTEND=noninteractive +RUN DEBIAN_BUSTER=$(grep buster /etc/apt/sources.list); \ + if [ -f /etc/apt/sources.list -a -n "$DEBIAN_BUSTER" ]; then \ + sed -i s/deb.debian.org/archive.debian.org/g /etc/apt/sources.list; \ + sed -i '/buster-updates/d' /etc/apt/sources.list; \ + fi RUN apt-get update \ && apt-get install -yq --no-install-recommends curl python3-pip build-essential git libclang-dev libdbus-1-dev cmake ninja-build libjack-dev \ && apt-get install -yq --no-install-recommends libfreetype6-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev libx11-xcb-dev libdrm-dev libglu1-mesa-dev libwayland-dev libwayland-egl1-mesa libgles2-mesa-dev libwayland-server0 libwayland-egl-backend-dev libxcb1-dev libxext-dev libfontconfig1-dev libxrender-dev libxcb-keysyms1-dev libxcb-image0-dev libxcb-shm0-dev libxcb-icccm4-dev '^libxcb.*-dev' libxcb-render-util0-dev libxcomposite-dev libgtk-3-dev \ @@ -22,7 +27,7 @@ RUN apt-get update \ && apt-get install -yq --no-install-recommends help2man clang-tidy desktop-file-utils RUN python3 -m pip install --upgrade pip \ && python3 -m pip install --upgrade certifi \ - && python3 -m pip install meson pyyaml Jinja2 + && python3 -m pip install meson==1.10.2 pyyaml Jinja2 WORKDIR /opt/jacktrip @@ -67,6 +72,10 @@ RUN if [ -n "$VST3SDK_DOWNLOAD_URL" ]; then \ COPY . ./ ARG MESON_ARGS="" ENV MESON_ARGS=$MESON_ARGS +# when set, keep debug symbols in the binary instead of stripping it. Optimisation +# level and defines are unchanged, so the generated code matches the release build. +ARG DEBUG_SYMBOLS="" +ENV DEBUG_SYMBOLS=$DEBUG_SYMBOLS ENV BUILD_PATH="/opt/jacktrip/builddir" RUN if [ -n "$QT_DOWNLOAD_URL" ]; then \ export QT_PATH="/opt/$(echo $QT_DOWNLOAD_URL | sed -e 's,.*/qt/\(qt-[.0-9]*\-[a-z]*\).*,\1,')"; \ @@ -75,13 +84,17 @@ RUN if [ -n "$QT_DOWNLOAD_URL" ]; then \ export CMAKE_PREFIX_PATH="$QT_PATH"; fi \ && if [ -n "$VST3SDK_DOWNLOAD_URL" ]; then \ export MESON_ARGS="-Dvst-sdkdir=${VST3SDK_INSTALL_PATH}/vst3sdk $MESON_ARGS"; fi \ + && if [ -n "$DEBUG_SYMBOLS" ]; then \ + export CFLAGS="-g3 -fno-omit-frame-pointer $CFLAGS"; \ + export CXXFLAGS="-g3 -fno-omit-frame-pointer $CXXFLAGS"; fi \ && export SSL_CERT_FILE=$(python3 -m certifi) \ && meson setup --buildtype release $MESON_ARGS $BUILD_PATH \ && meson compile -C $BUILD_PATH -v \ && mkdir -p $BUILD_PATH/src/vst3 \ - && strip $BUILD_PATH/jacktrip \ - && if [ -n "$VST3SDK_DOWNLOAD_URL" ]; then \ - strip $BUILD_PATH/src/vst3/JackTrip.vst3; fi + && if [ -z "$DEBUG_SYMBOLS" ]; then \ + strip $BUILD_PATH/jacktrip; \ + if [ -n "$VST3SDK_DOWNLOAD_URL" ]; then \ + strip $BUILD_PATH/src/vst3/JackTrip.vst3; fi; fi FROM scratch AS artifact diff --git a/linux/README.md b/linux/README.md index 072397e..6e6ee33 100644 --- a/linux/README.md +++ b/linux/README.md @@ -13,7 +13,7 @@ dnf install -y qt6-qtbase qt6-qtbase-common qt6-qtbase-gui qt6-qtsvg qt6-qtwebso For Debian or Ubuntu: ``` -apt install -y libqt6core6 libqt6gui6 libqt6network6 libqt6widgets6 libqt6qml6 libqt6qmlcore6 libqt6quick6 libqt6quickcontrols2-6 libqt6svg6 libqt6webchannel6 libqt6webengine6-data libqt6webenginecore6 libqt6webenginecore6-bin libqt6webenginequick6 libqt6websockets6 libqt6shadertools6 qt6-qpa-plugins qml6-module-qtquick-controls qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qt5compat-graphicaleffects qml6-module-qtwebchannel qml6-module-qtwebengine qml6-module-qtquick-window qml6-module-qtquick-dialogs libjack-jackd2-0 librtaudio6 libxcb-cursor0 +apt install -y libqt6core6 libqt6gui6 libqt6network6 libqt6widgets6 libqt6qml6 libqt6qmlcore6 libqt6quick6 libqt6quickcontrols2-6 libqt6quickdialogs2-6 libqt6svg6 libqt6webchannel6 libqt6webengine6-data libqt6webenginecore6 libqt6webenginecore6-bin libqt6webenginequick6 libqt6websockets6 libqt6shadertools6 qt6-qpa-plugins qml6-module-qtquick-controls qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qt5compat-graphicaleffects qml6-module-qtwebchannel qml6-module-qtwebengine qml6-module-qtquick-window qml6-module-qtquick-dialogs libjack-jackd2-0 librtaudio6 libxcb-cursor0 ``` To install JackTrip as a Linux desktop application: diff --git a/linux/debug/README.md b/linux/debug/README.md new file mode 100644 index 0000000..7dadb96 --- /dev/null +++ b/linux/debug/README.md @@ -0,0 +1,101 @@ +# JackTrip debug build — how to capture a crash + +This is the same JackTrip as the matching regular Linux release, rebuilt with full +debug symbols so that a crash produces a usable stack trace. It is built with the +same compiler optimisations and the same feature set as the release build — the only +difference is that the debug information needed to read a crash dump is left in the +binary, which is why the file is much larger than usual. + +`BUILD-INFO.txt` records exactly which commit it was built from. Please include that +file, or the version it names, in any report. + +It needs the same Qt 6 packages as the regular JackTrip Linux build — `INSTALL.md` +in this archive lists them. If your normal JackTrip runs, this one will too. + +If you unpacked this with a graphical archive tool, the helper scripts may have lost +their executable bit. Restore it with: + +```bash +chmod +x *.sh +``` + +--- + +## Option A — run it under gdb (easiest, and gives us the most useful result) + +This is the recommended path: gdb catches the crash as it happens and writes both a +readable backtrace and a core file. + +```bash +sudo apt install gdb # or: sudo dnf install gdb +./run-under-gdb.sh +``` + +This opens the normal JackTrip window, exactly as if you had started JackTrip by +hand — gdb just watches it from the outside. Reproduce the problem the way you +normally hit it, repeating until the crash happens; intermittent crashes can take +several tries. + +When it crashes, the script leaves these files in the current directory: + +- `jacktrip-gdb-.log` — the backtrace. **This is the important one.** +- `core.` — the core dump, if gdb managed to write one. + +Send us the `.log` file. If a core file is there too, compress it first +(`gzip core.*`) — it is large, and it compresses down a lot. + +If JackTrip exits normally, or you stop it with Ctrl-C, the log says so instead of +reporting a crash. Just run the script again. + +--- + +## Option B — plain core dump, no gdb + +Use this if you would rather not run under gdb. Ubuntu hands core dumps to apport by +default, which does not keep them anywhere useful for a program that was not +installed from a package, so the core dump destination has to be changed first. + +```bash +sudo ./enable-core-dumps.sh # asks for your password; change is temporary +./run-with-coredump.sh +``` + +Reproduce the crash. Core files are written to `/tmp/cores/`, named +`core...`. + +Send us the core file for `jacktrip` (gzip it first — `gzip /tmp/cores/core.jacktrip.*`) +along with `jacktrip-run-.log`. + +The core dump setting reverts on reboot. To undo it right away: + +```bash +sudo ./enable-core-dumps.sh --restore +``` + +--- + +## What to send back + +In rough order of usefulness: + +1. `jacktrip-gdb-.log` (Option A) — a backtrace is often enough on its own. +2. The gzipped core file, if you have one. +3. The console log (`jacktrip-run-.log`) — verbose output from the run that + crashed, which shows how far things got. +4. `BUILD-INFO.txt`, so we know exactly which build produced the crash. +5. What you were doing, whether it had ever worked before, which audio device and + backend you were using, and roughly how often it happens. + +The logs contain JackTrip's normal console output, which names your audio devices and +the studio you connected to. Have a look through them before posting them anywhere +public. + +## Notes + +- JackTrip's UI is built on QtWebEngine, which runs helper processes. If the crash is + in one of those rather than in JackTrip itself, you may get a core file named + `core.QtWebEngineProc...` — that is still useful, please send it. +- Nothing here installs anything or replaces an existing JackTrip installation. It all + runs from the directory you unpacked it into. + +Please report any security concerns to vulnerabilities@jacktrip.org diff --git a/linux/debug/crash.gdb b/linux/debug/crash.gdb new file mode 100644 index 0000000..300a143 --- /dev/null +++ b/linux/debug/crash.gdb @@ -0,0 +1,39 @@ +set pagination off +set confirm off +set backtrace past-main on +# Signals JackTrip and its Qt/Chromium threads use routinely. Stopping on any of +# these would end the session before the real crash ever happens. +handle SIGPIPE nostop noprint pass +handle SIGUSR1 nostop noprint pass +handle SIGUSR2 nostop noprint pass +run +# $_exitcode is void if the program did not exit on its own, i.e. it was +# stopped by a fatal signal. +if $_isvoid($_exitcode) + # si_signo 2 is SIGINT: the user stopped it from the keyboard, which is + # not a crash and produces no useful backtrace. + set $sig = 0 + if !$_isvoid($_siginfo) + set $sig = $_siginfo.si_signo + end + if $sig == 2 + echo \n---------- JackTrip was interrupted, not a crash ----------\n + else + echo \n========== CRASH DETAILS BELOW ==========\n + info program + echo \n---------- faulting thread ----------\n + bt full + echo \n---------- registers ----------\n + info registers + echo \n---------- all threads ----------\n + thread apply all bt full + echo \n---------- loaded libraries ----------\n + info sharedlibrary + echo \n---------- writing core file ----------\n + generate-core-file + echo \n========== END OF CRASH DETAILS ==========\n + end +else + echo \n---------- JackTrip exited on its own, no crash was captured ----------\n +end +quit diff --git a/linux/debug/enable-core-dumps.sh b/linux/debug/enable-core-dumps.sh new file mode 100755 index 0000000..090833d --- /dev/null +++ b/linux/debug/enable-core-dumps.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Point kernel core dumps at /tmp/cores instead of apport, which on Ubuntu does not +# keep usable core files for programs that were not installed from a package. +# +# sudo ./enable-core-dumps.sh enable +# sudo ./enable-core-dumps.sh --restore put the previous setting back +# +# The change is not permanent: it reverts by itself on the next reboot. + +set -euo pipefail + +BACKUP="/var/tmp/jacktrip-core-pattern.bak" +CORE_DIR="/tmp/cores" +PATTERN="$CORE_DIR/core.%e.%p.%t" + +if [ "$(id -u)" -ne 0 ]; then + echo "This script needs root. Run it as: sudo $0 ${*:-}" >&2 + exit 1 +fi + +if [ "${1:-}" = "--restore" ]; then + if [ -f "$BACKUP" ]; then + sysctl -w "kernel.core_pattern=$(cat "$BACKUP")" >/dev/null + rm -f "$BACKUP" + echo "Restored core_pattern to: $(cat /proc/sys/kernel/core_pattern)" + else + echo "No saved setting found. Rebooting also restores the default." >&2 + exit 1 + fi + exit 0 +fi + +if [ ! -f "$BACKUP" ]; then + cat /proc/sys/kernel/core_pattern > "$BACKUP" +fi + +mkdir -p "$CORE_DIR" +chmod 1777 "$CORE_DIR" +sysctl -w "kernel.core_pattern=$PATTERN" >/dev/null + +echo "Core dumps will now be written to $CORE_DIR/" +echo " current pattern: $(cat /proc/sys/kernel/core_pattern)" +echo " previous pattern saved in $BACKUP" +echo +echo "Now run ./run-with-coredump.sh (as your normal user, not with sudo)." diff --git a/linux/debug/run-under-gdb.sh b/linux/debug/run-under-gdb.sh new file mode 100755 index 0000000..3966f10 --- /dev/null +++ b/linux/debug/run-under-gdb.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Run the JackTrip debug build under gdb so that a crash produces a readable +# backtrace (and a core file, if gdb can write one). +# +# Usage: ./run-under-gdb.sh [extra jacktrip arguments] + +set -uo pipefail +cd "$(dirname "$(readlink -f "$0")")" + +if ! command -v gdb >/dev/null 2>&1; then + echo "gdb is not installed. Install it with: sudo apt install gdb" >&2 + exit 1 +fi + +TIMESTAMP=$(date +%Y%m%d-%H%M%S) +LOG="jacktrip-gdb-$TIMESTAMP.log" + +# Allow gdb to write a core file of unlimited size. +ulimit -c unlimited + +# With no arguments JackTrip starts its normal window, which is where the crashes +# we are chasing happen. Passing any option at all puts it into command line mode +# instead, so --gui is added explicitly here. Extra arguments are still honoured +# for anyone who needs them. +if [ "$#" -eq 0 ]; then + set -- --gui +fi + +echo "Logging to $LOG" +echo "Reproduce the problem now: connect to a studio as you normally would." +echo "If it does not crash, quit JackTrip and run this script again." +echo + +gdb -q -batch -x crash.gdb --args ./jacktrip "$@" 2>&1 | tee "$LOG" + +echo +echo "===================================================================" +if grep -q "CRASH DETAILS BELOW" "$LOG"; then + echo "A crash was captured." +elif grep -q "was interrupted, not a crash" "$LOG"; then + echo "No crash this time — JackTrip was interrupted from the keyboard." +else + echo "No crash this time — JackTrip exited on its own." +fi +echo "Saved log: $(pwd)/$LOG" +if ls core.* >/dev/null 2>&1; then + echo "Core file(s):" + ls -lh core.* | awk '{print " "$9" ("$5")"}' + echo "Please gzip the core file before sending it: gzip core.*" +else + echo "No core file was written (that is OK — the log is the important part)." +fi +echo "Please send the log file back to us." +echo "===================================================================" diff --git a/linux/debug/run-with-coredump.sh b/linux/debug/run-with-coredump.sh new file mode 100755 index 0000000..5b3d545 --- /dev/null +++ b/linux/debug/run-with-coredump.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Run the JackTrip debug build with core dumps enabled and verbose logging. +# Run ./enable-core-dumps.sh (with sudo) once before using this. +# +# Usage: ./run-with-coredump.sh [extra jacktrip arguments] + +set -uo pipefail +cd "$(dirname "$(readlink -f "$0")")" + +PATTERN=$(cat /proc/sys/kernel/core_pattern) +case "$PATTERN" in + /*) : ;; + *) + echo "WARNING: core dumps are still being sent to apport:" + echo " $PATTERN" + echo "A crash will most likely NOT leave a core file behind." + echo "Run 'sudo ./enable-core-dumps.sh' first, or use ./run-under-gdb.sh instead." + echo + read -r -p "Continue anyway? [y/N] " reply + [ "$reply" = "y" ] || [ "$reply" = "Y" ] || exit 1 + ;; +esac + +ulimit -c unlimited + +# JackTrip only starts its normal window when it is given no options at all, so +# --gui is added explicitly. Extra arguments are still honoured. +if [ "$#" -eq 0 ]; then + set -- --gui +fi + +TIMESTAMP=$(date +%Y%m%d-%H%M%S) +LOG="jacktrip-run-$TIMESTAMP.log" + +echo "Core dump pattern: $PATTERN" +echo "Logging to $LOG" +echo "Reproduce the crash now (connect to a studio)." +echo + +./jacktrip "$@" 2>&1 | tee "$LOG" +STATUS=${PIPESTATUS[0]} + +echo +echo "===================================================================" +echo "JackTrip exited with status $STATUS" +if [ "$STATUS" -ge 128 ]; then + echo "That looks like a crash (signal $((STATUS - 128)))." +fi +echo "Saved log: $(pwd)/$LOG" +case "$PATTERN" in + /*) CORE_DIR=$(dirname "$PATTERN") ;; + *) CORE_DIR="/tmp/cores" ;; +esac +CORES=$(ls -t "$CORE_DIR"/core.* 2>/dev/null | head -5) +if [ -n "$CORES" ]; then + echo "Recent core files:" + ls -lh $CORES | awk '{print " "$9" ("$5")"}' + echo "Please gzip the newest one before sending it: gzip " +else + echo "No core file found in $CORE_DIR/." +fi +echo "===================================================================" diff --git a/meson.build b/meson.build index f0c21fc..65bdd95 100644 --- a/meson.build +++ b/meson.build @@ -159,6 +159,9 @@ else endif deps += qt_core_deps +# Get Qt installation prefix for cmake subprojects +qt_prefix = run_command(qmake, '-query', 'QT_INSTALL_PREFIX', check : true).stdout().strip() + if get_option('nogui') == true or (get_option('noclassic') == true and get_option('novs') == true) # command line only defines += '-DNO_GUI' @@ -370,6 +373,7 @@ if get_option('libsamplerate').allowed() opt_var.add_cmake_defines({'CMAKE_BUILD_TYPE': 'Debug'}) endif opt_var.add_cmake_defines({'CMAKE_POSITION_INDEPENDENT_CODE': 'ON'}) + opt_var.add_cmake_defines({'CMAKE_POLICY_VERSION_MINIMUM': '3.5'}) libsamplerate_subproject = cmake.subproject('libsamplerate', options: opt_var) libsamplerate_dep = libsamplerate_subproject.dependency('samplerate') found_libsamplerate = libsamplerate_dep.found() @@ -383,6 +387,107 @@ if found_libsamplerate deps += libsamplerate_dep endif +# WebRTC Data Channel Support (requires libdatachannel) +found_libdatachannel = false +libdatachannel_extra_deps = [] +if get_option('libdatachannel').allowed() + # First try to find libdatachannel as a system dependency + libdatachannel_dep = dependency('libdatachannel', required: false) + if libdatachannel_dep.found() + found_libdatachannel = true + else + # Try to build libdatachannel as a subproject + opt_var = cmake.subproject_options() + if get_option('buildtype') == 'release' + opt_var.add_cmake_defines({'CMAKE_BUILD_TYPE': 'Release'}) + else + opt_var.add_cmake_defines({'CMAKE_BUILD_TYPE': 'Debug'}) + endif + opt_var.add_cmake_defines({'CMAKE_POSITION_INDEPENDENT_CODE': 'ON'}) + # Build static library only + opt_var.add_cmake_defines({'BUILD_SHARED_LIBS': 'OFF'}) + # Disable features we don't need to speed up build + opt_var.add_cmake_defines({'NO_WEBSOCKET': 'ON'}) + opt_var.add_cmake_defines({'NO_MEDIA': 'ON'}) + opt_var.add_cmake_defines({'NO_EXAMPLES': 'ON'}) + opt_var.add_cmake_defines({'NO_TESTS': 'ON'}) + opt_var.add_cmake_defines({'USE_GNUTLS': '0'}) + opt_var.add_cmake_defines({'USE_NICE': '0'}) + opt_var.add_cmake_defines({'CMAKE_PREFIX_PATH': qt_prefix}) + libdatachannel_subproject = cmake.subproject('libdatachannel', options: opt_var, required: false) + if libdatachannel_subproject.found() + libdatachannel_dep = libdatachannel_subproject.dependency('datachannel') + found_libdatachannel = libdatachannel_dep.found() + # When building as a static library, we need to also link the private + # dependencies (libjuice and usrsctp) that libdatachannel uses internally + if found_libdatachannel + libdatachannel_extra_deps += libdatachannel_subproject.dependency('juice') + libdatachannel_extra_deps += libdatachannel_subproject.dependency('usrsctp') + endif + endif + if not found_libdatachannel and not get_option('libdatachannel').auto() + error('libdatachannel requested but could not be configured') + endif + endif +endif +if found_libdatachannel + defines += '-DWEBRTC_SUPPORT' + deps += libdatachannel_dep + deps += libdatachannel_extra_deps + src += [ + 'src/webrtc/WebRtcDataProtocol.cpp', + 'src/webrtc/WebRtcPeerConnection.cpp', + 'src/webrtc/WebRtcSignalingProtocol.cpp', + 'src/webrtc/WebSocketSignalingConnection.cpp' + ] + moc_h += [ + 'src/webrtc/WebRtcDataProtocol.h', + 'src/webrtc/WebRtcPeerConnection.h', + 'src/webrtc/WebRtcSignalingProtocol.h', + 'src/webrtc/WebSocketSignalingConnection.h' + ] +endif + +# WebTransport support (requires msquic) +found_msquic = false +if get_option('msquic').allowed() + # Try to get msquic dependency (from system or subproject with native meson.build) + msquic_dep = dependency('msquic', required: false) + if not msquic_dep.found() + # Try the subproject with native meson.build overlay + msquic_subproject = subproject('msquic', required: get_option('msquic').enabled(), default_options: ['qt_prefix=' + qt_prefix]) + if msquic_subproject.found() + msquic_dep = msquic_subproject.get_variable('msquic_dep') + endif + endif + found_msquic = msquic_dep.found() +endif +if found_msquic + defines += '-DWEBTRANSPORT_SUPPORT' + deps += msquic_dep + src += [ + 'src/http3/Http3Protocol.cpp', + 'src/http3/Http3Server.cpp', + 'src/webtransport/WebTransportSession.cpp', + 'src/webtransport/WebTransportDataProtocol.cpp' + ] + moc_h += [ + 'src/webtransport/WebTransportSession.h', + 'src/webtransport/WebTransportDataProtocol.h' + ] + # msquic is statically linked against its own OpenSSL (libcrypto.a/libssl.a). + # Without this flag those ~2700 OpenSSL symbols land in the executable's dynamic + # symbol table and interpose the system libssl/libcrypto that Qt dlopens at runtime. + # The result: Qt parses an X509 with one OpenSSL while SSL_CTX_use_certificate runs + # against the other, so server-side TLS fails with "Error loading local certificate" + # (empty OpenSSL detail) and the WebRTC/ping handshake hangs. Keeping static-archive + # symbols local stops the interposition. GNU ld / lld only; harmless on the BSD-style + # macOS linker path, so restrict to Linux where the conflict occurs. + if host_machine.system() == 'linux' + link_args += '-Wl,--exclude-libs,ALL' + endif +endif + if host_machine.system() == 'darwin' src += ['src/NoNap.mm'] # Adding CoreAudio here is a workaround and should be removed @@ -462,4 +567,6 @@ summary({'Application ID': application_id, 'GUI': not get_option('nogui'), 'WAIR': get_option('wair'), 'Sample rate conversions': found_libsamplerate, + 'WebRTC Data Channels': found_libdatachannel, + 'WebTransport API': found_msquic, 'Manpage': help2man.found()}, bool_yn: true, section: 'Configuration') diff --git a/meson_options.txt b/meson_options.txt index a1850ae..4d478d9 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -17,4 +17,8 @@ option('buildinfo', type : 'string', value : '', yield : true, description: 'Add option('vst-libdir', type : 'string', value : '', yield : true, description : 'Directory with VST SDK3 libraries (e.g. libsdk.a, libbase.a)') option('vst-sdkdir', type : 'string', value : '', yield : true, - description : 'Directory with VST SDK3 headers (e.g. public.sdk/source/vst/hosting/module.h)') \ No newline at end of file + description : 'Directory with VST SDK3 headers (e.g. public.sdk/source/vst/hosting/module.h)') +option('libdatachannel', type : 'feature', value : 'disabled', + description : 'Build with WebRTC data channel support (requires libdatachannel)') +option('msquic', type : 'feature', value : 'disabled', + description : 'Build with WebTransport support (requires msquic)') \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 501de72..286f79a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,6 +16,7 @@ nav: - Development Tools: - Formatting: DevTools/Formatting.md - Static Analysis: DevTools/StaticAnalysis.md + - Network Protocol: Documentation/NetworkProtocol.md - Write Documentation: Documentation/MkDocs.md - About: - Contributors: About/Contributors.md diff --git a/plans/defer-webtransport-worker-creation.md b/plans/defer-webtransport-worker-creation.md new file mode 100644 index 0000000..f68be3d --- /dev/null +++ b/plans/defer-webtransport-worker-creation.md @@ -0,0 +1,160 @@ +# Plan: Defer WebTransport worker creation until session is established + +## Background / motivation + +The HUB server currently allocates a full `JackTripWorker` for **every incoming QUIC +connection**, at `QUIC_LISTENER_EVENT_NEW_CONNECTION` — i.e. *before* the QUIC/TLS +handshake and HTTP/3 `CONNECT` complete. + +Observed symptom: Firefox on Fedora opens **two** QUIC connections from the same client +IP (different source ports). One completes the WebTransport handshake and streams audio; +the other is abandoned and dies with `SHUTDOWN_INITIATED_BY_TRANSPORT` ("Transport +shutdown"). Because a worker is created up-front, the abandoned connection: + +- transiently bumps `mTotalRunningThreads` (the misleading `Total Running Threads: 2` + for a single client), +- allocates a heavyweight `JackTrip` instance (audio interface, ring buffers) that is + immediately torn down, +- produces "session failed" / shutdown log spam. + +This is benign today (the original wrong-slot cleanup bug was fixed by assigning the +worker's slot id in `createWorker`, see `UdpHubListener::createWorker` → +`worker->setID(id)`), but it is wasteful and noisy. It also makes the server fragile to +any client (or scanner/probe) that opens speculative or half-open QUIC connections, not +just Firefox. + +**Goal:** only the lightweight `WebTransportSession` is created at `NEW_CONNECTION`. The +heavyweight `JackTripWorker` + slot allocation is deferred until the session actually +reaches `sessionEstablished` (HTTP/3 `CONNECT` accepted with status 200). Connections that +never establish never consume a worker slot or bump the thread count. + +## Current flow (for reference) + +1. `Http3Server` listener callback `QUIC_LISTENER_EVENT_NEW_CONNECTION` + (`src/http3/Http3Server.cpp:239`) accepts the connection, sets its config, then invokes + `mConnectionCallback(connection, addr, port)`. +2. That callback is `UdpHubListener::createWebTransportWorker(...)` + (`src/UdpHubListener.cpp:991`), which: + - calls `createWorker()` → finds a free slot, `new JackTripWorker`, `setID(id)`, + `mTotalRunningThreads++`, stores in `mJTWorkers[id]`; + - `worker->moveToThread(listenerThread)`; + - connects `signalRemoveThread → handleWorkerRemoval`; + - `new WebTransportSession(quicApi, connection, addr, port, nullptr)` — the session + registers itself as the QUIC connection callback handler in its constructor + (`src/webtransport/WebTransportSession.cpp:122`); + - `session->moveToThread(listenerThread)`; + - `worker->createWebTransportSession(session)` — reparents session to worker and wires + `sessionEstablished/Closed/Failed` to the worker's slots + (`src/JackTripWorker.cpp:607`). +3. On `sessionEstablished`, `JackTripWorker::onWebTransportSessionEstablished()` configures + `JackTrip` (ports, channels, protocol) but does **not** start audio yet. +4. On the first datagram, `receivedFirstPacketWebTransport → processPeerSettings → + startProcess` actually starts the audio pipeline (`mRunning = true`). + +The key insight: **the `WebTransportSession` must exist at `NEW_CONNECTION`** (it is the +QUIC callback handler — without it, nothing services the handshake). But the +`JackTripWorker` is only needed once the session is established. + +## Proposed design + +### Ownership of the pending session + +Introduce a "pending session" stage owned by `UdpHubListener` (it already owns the +`Http3Server` and is the connection delegate): + +- Add a container, e.g. `QHash` or simply a + `QSet mPendingWtSessions`, guarded by `mMutex`. +- At `NEW_CONNECTION` (replace `createWebTransportWorker`): + 1. `auto* session = new WebTransportSession(quicApi, connection, addr, port, nullptr);` + 2. `session->moveToThread(this->thread());` + 3. Connect, with `Qt::QueuedConnection`: + - `session->sessionEstablished → UdpHubListener::onWebTransportSessionEstablished(session)` + - `session->sessionFailed → UdpHubListener::onPendingSessionGone(session)` + - `session->sessionClosed → UdpHubListener::onPendingSessionGone(session)` + 4. Insert into `mPendingWtSessions`. + 5. **Do not** call `createWorker()`, do not bump `mTotalRunningThreads`. + + Because `sessionEstablished` is connected via a queued connection, it is delivered on + the listener thread's event loop, so worker creation happens on the right thread. + +### Promotion to a worker (on establish) + +`UdpHubListener::onWebTransportSessionEstablished(WebTransportSession* session)`: + +1. Under `mMutex`, remove `session` from `mPendingWtSessions`. If it was not present + (already failed/closed), bail out. +2. `int id = createWorker(tempName);` (this assigns slot + `setID(id)` + + `mTotalRunningThreads++`). + - If `id < 0` (no free slots), `session->close(); session->deleteLater();` and return — + reject gracefully. +3. `JackTripWorker* worker = mJTWorkers->at(id);` +4. `worker->moveToThread(this->thread());` +5. `connect(worker, signalRemoveThread, this, handleWorkerRemoval, QueuedConnection);` +6. **Re-wire the session to the worker.** The session is already CONNECTED, so the + worker's `createWebTransportSession()` "already connected → onWebTransportSessionEstablished" + fast-path (`src/JackTripWorker.cpp:644`) will run and configure `JackTrip`. Verify the + signal connections set up inside `createWebTransportSession` (sessionEstablished/Closed/ + Failed → worker slots) plus the datagram callback are correct given the session is + already established. The `isConnected()` branch already exists for exactly this case. +7. Disconnect the temporary `UdpHubListener`-side session signal connections from step + "pending" (so they don't double-fire alongside the worker's connections). + +### Cleanup of a pending session that never establishes (on fail/close) + +`UdpHubListener::onPendingSessionGone(WebTransportSession* session)`: + +1. Under `mMutex`, if `session` is still in `mPendingWtSessions`, erase it and + `session->deleteLater();`. No worker, no thread-count change, minimal logging + (gate behind `gVerboseFlag`). +2. If it is **not** in the set, it was already promoted to a worker — ignore (the worker's + own `onWebTransportSessionFailed/Closed` path handles teardown). + +### Thread-safety notes + +- The session is created on the msquic thread (the listener callback runs there), then + `moveToThread(this->thread())`. All subsequent signal handling is queued onto the + listener thread — same pattern as today. +- `mPendingWtSessions` mutations must be under `mMutex` (consistent with `mJTWorkers` + access). +- There is an inherent race: `sessionFailed` and `sessionEstablished` could both be + emitted. Using set membership as the single source of truth (whoever removes it first + wins) resolves it: establish promotes, fail/close frees, and the second handler sees the + session is no longer pending and no-ops. + +## Files to change + +| File | Change | +|------|--------| +| `src/UdpHubListener.h` | Add `QSet mPendingWtSessions;` member; declare `onWebTransportSessionEstablished(WebTransportSession*)` and `onPendingSessionGone(WebTransportSession*)` slots; keep/retire `createWebTransportWorker`. | +| `src/UdpHubListener.cpp` | Replace `createWebTransportWorker` body with "create pending session only"; add the two new slots; ensure the `Http3Server` connection callback (`src/UdpHubListener.cpp:289`) calls the new entry point. | +| `src/JackTripWorker.cpp` / `.h` | Likely no structural change — `createWebTransportSession()` already handles the "already connected" case. Confirm the established-on-attach path fully configures and that no second `sessionEstablished` is required. | +| `src/webtransport/WebTransportSession.*` | No change expected; it already emits `sessionEstablished/Failed/Closed` and self-registers as the QUIC handler. | + +## Testing / verification + +1. **Single Chrome/Safari client (baseline):** one QUIC connection → `Total Running + Threads: 1`, audio streams. No regression. +2. **Firefox on Fedora (repro case):** two QUIC connections; the abandoned one should now + produce **no** worker and **no** thread-count bump. Expect at most a single gated + "pending session failed" verbose line, and `Total Running Threads: 1`. +3. **Slot exhaustion:** with `-p N` and N active clients, an additional establishing + session should be rejected cleanly (`session->close()`), not crash. +4. **Rapid connect/disconnect:** client that connects and immediately drops mid-handshake + — pending session is freed, no leak. Watch for the + `WebTransportDataProtocol::stop: thread did not finish within 1s` warning (should not + appear). +5. **Probe/scan:** point a QUIC scanner at the port — connections that never send a + `CONNECT` should be reaped by msquic idle timeout with no worker allocation. +6. Run under `-V` (verbose) to confirm the gated diagnostics tell a coherent story: + pending-created → established → worker N, or pending-created → gone. + +## Out of scope / notes + +- This does **not** address *why* Firefox-on-Fedora opens a second connection — that is a + client/browser behavior (suspected Firefox QUIC connection racing) and cannot be fixed + server-side. This plan only makes the server robust and quiet in the face of it. +- The same up-front-allocation pattern exists for **WebRTC** (`createWebRtcWorker`, + `src/UdpHubListener.cpp:948`). If WebRTC shows similar speculative-connection churn, a + parallel deferral could be applied, but it is not covered here. +- Keep the `mID`/slot-assignment fix (`createWorker` → `setID(id)`) regardless; it is + independent and already correct. diff --git a/src/AudioInterface.cpp b/src/AudioInterface.cpp index 044cdbb..27f8274 100644 --- a/src/AudioInterface.cpp +++ b/src/AudioInterface.cpp @@ -74,6 +74,9 @@ AudioInterface::AudioInterface(QVarLengthArray InputChans, , mBitResolutionMode(AudioBitResolution) , mSampleRate(gDefaultSampleRate) , mBufferSizeInSamples(gDefaultBufferSizeInSamples) + , mAllocatedFrames(0) + , mBufferSizeMismatchReported(false) + , mSizeInBytesPerChannel(0) , mMonitorQueuePtr(NULL) , mAudioInputPacket(NULL) , mAudioOutputPacket(NULL) @@ -115,8 +118,13 @@ AudioInterface::~AudioInterface() void AudioInterface::setup(bool /*verbose*/) { // Allocate buffer memory to read and write - mSizeInBytesPerChannel = getSizeInBytesPerChannel(); + // Take a single snapshot of the period size and derive every allocation below + // from it. Backends such as JACK report the server's *current* period size, + // which can change while we are running, so remember what we allocated for and + // never size or index these buffers from a fresh query afterwards. int nframes = getBufferSizeInSamples(); + mAllocatedFrames = nframes; + mSizeInBytesPerChannel = size_t(nframes) * getAudioBitResolution() / 8; int size_audio_input = int(mSizeInBytesPerChannel * mInputChans.size()); int size_audio_output = int(mSizeInBytesPerChannel * mOutputChans.size()); #ifdef WAIR // WAIR @@ -173,6 +181,14 @@ void AudioInterface::setup(bool /*verbose*/) //******************************************************************************* size_t AudioInterface::getSizeInBytesPerChannel() const { + if (mAllocatedFrames > 0) { + // Once setup() has run this must stay fixed: the network packet size and + // the ring buffer slot sizes are derived from it, and packets are copied + // in and out of buffers that were allocated for this many bytes. Deriving + // it from the current period size instead would overrun them if the audio + // server changed its period after we were set up. + return mSizeInBytesPerChannel; + } return (getBufferSizeInSamples() * getAudioBitResolution() / 8); } @@ -185,15 +201,33 @@ void AudioInterface::callback(QVarLengthArray& in_buffer, this->audioOutputCallback(out_buffer, n_frames); } +//******************************************************************************* +bool AudioInterface::framesFitAllocatedBuffers(unsigned int n_frames) +{ + if (n_frames <= mAllocatedFrames) { + return true; + } + // The backend handed us a larger period than setup() allocated for. The packet + // and process buffers are all indexed by n_frames, so carrying on would write + // several kilobytes past the end of them and corrupt the heap. Drop the period + // instead. The backend is expected to notice the change and stop the stream; + // this is the last line of defence if it does not. + if (!mBufferSizeMismatchReported) { + mBufferSizeMismatchReported = true; + std::cerr << "*** AudioInterface: the audio period grew to " << n_frames + << " frames, but the audio buffers were allocated for " + << mAllocatedFrames << ". Dropping audio to avoid corrupting memory.\n"; + } + return false; +} + //******************************************************************************* void AudioInterface::audioInputCallback(QVarLengthArray& in_buffer, unsigned int n_frames) { // in_buffer is "in" from local audio hardware - if (getBufferSizeInSamples() < n_frames) { // allocated in constructor above - std::cerr << "*** AudioInterface::audioInputCallback n_frames = " << n_frames - << " larger than expected = " << getBufferSizeInSamples() << "\n"; - exit(1); + if (!framesFitAllocatedBuffers(n_frames)) { + return; } #ifndef WAIR @@ -235,11 +269,13 @@ void AudioInterface::audioInputCallback(QVarLengthArray& in_buffer, void AudioInterface::audioOutputCallback(QVarLengthArray& out_buffer, unsigned int n_frames) { - // in_buffer is "in" from local audio hardware - if (getBufferSizeInSamples() < n_frames) { // allocated in constructor above - std::cerr << "*** AudioInterface::audioOutputCallback n_frames = " << n_frames - << " larger than expected = " << getBufferSizeInSamples() << "\n"; - exit(1); + // out_buffer is "out" to local audio hardware + if (!framesFitAllocatedBuffers(n_frames)) { + // the hardware buffers are ours to fill, so hand back silence + for (int i = 0; i < mOutputChans.size(); i++) { + std::memset(out_buffer[i], 0, sizeof(sample_t) * n_frames); + } + return; } // 1) First, process incoming packets @@ -406,6 +442,13 @@ void AudioInterface::audioOutputCallback(QVarLengthArray& out_buffer, void AudioInterface::broadcastCallback(QVarLengthArray& mon_buffer, unsigned int n_frames) { + if (!framesFitAllocatedBuffers(n_frames)) { + for (int i = 0; i < mOutputChans.size(); i++) { + std::memset(mon_buffer[i], 0, sizeof(sample_t) * n_frames); + } + return; + } + /// \todo cast *mInBuffer[i] to the bit resolution // Output Process (from NETWORK to JACK) // ---------------------------------------------------------------- @@ -487,6 +530,12 @@ void AudioInterface::computeProcessToNetwork(QVarLengthArray& in_buff // Concatenate all the channels from jack to form packet #ifdef WAIR // WAIR + +#define INGAIN \ + (0.9999) // 0.9999 because 1.0 can saturate the fixed pt rounding on output + +#define COMBGAIN (1.0) + if (mNumNetRevChans) for (int i = 0; i < mNumNetRevChans; i++) { sample_t* tmp_sample = @@ -498,9 +547,6 @@ void AudioInterface::computeProcessToNetwork(QVarLengthArray& in_buff // Change the bit resolution on each sample // Add the input jack buffer to the buffer resulting from the output // process -#define INGAIN \ - (0.9999) // 0.9999 because 1.0 can saturate the fixed pt rounding on output -#define COMBGAIN (1.0) tmp_result = INGAIN * tmp_sample[j] + COMBGAIN * tmp_process_sample[j]; fromSampleToBitConversion( &tmp_result, diff --git a/src/AudioInterface.h b/src/AudioInterface.h index 886ad59..40b03a1 100644 --- a/src/AudioInterface.h +++ b/src/AudioInterface.h @@ -291,6 +291,14 @@ class AudioInterface virtual QVarLengthArray getOutputChannels() const { return mOutputChans; } virtual inputMixModeT getInputMixMode() const { return mInputMixMode; } virtual uint32_t getBufferSizeInSamples() const { return mBufferSizeInSamples; } + /** \brief Get the number of frames per period the audio buffers were allocated for + * + * This is a snapshot taken by setup(). It can differ from + * getBufferSizeInSamples() if the audio server changes its period size + * afterwards. Anything that indexes into the buffers allocated by setup() + * must be bounded by this value, not by the current period size. + */ + uint32_t getAllocatedBufferSizeInSamples() const { return mAllocatedFrames; } virtual uint32_t getDeviceID() const { return mDeviceID; } virtual std::string getInputDevice() const { return mInputDeviceName; } virtual std::string getOutputDevice() const { return mOutputDeviceName; } @@ -328,6 +336,14 @@ class AudioInterface /// \brief Compute the process to send packets void computeProcessToNetwork(QVarLengthArray& in_buffer, unsigned int n_frames); + /** \brief Check that a period handed to us by the audio backend fits the + * buffers that setup() allocated + * + * Returns false, and logs once, if it does not. Callers must bail out in + * that case: every buffer allocated by setup() is indexed with n_frames, so + * processing a larger period would write past the end of them. + */ + bool framesFitAllocatedBuffers(unsigned int n_frames); QVarLengthArray mInputChans; QVarLengthArray mOutputChans; @@ -345,7 +361,9 @@ class AudioInterface uint32_t mDeviceID; ///< RTAudio DeviceID std::string mInputDeviceName, mOutputDeviceName; ///< RTAudio device names uint32_t mBufferSizeInSamples; ///< Buffer size in samples - size_t mSizeInBytesPerChannel; ///< Size in bytes per audio channel + uint32_t mAllocatedFrames; ///< Frames per period the buffers below were sized for + bool mBufferSizeMismatchReported; ///< True once a period size growth has been logged + size_t mSizeInBytesPerChannel; ///< Size in bytes per audio channel QVector > mProcessPluginsFromNetwork; ///< Vector of ProcessPlugins QVector > diff --git a/src/AudioTester.cpp b/src/AudioTester.cpp index 64e141e..a20ccec 100644 --- a/src/AudioTester.cpp +++ b/src/AudioTester.cpp @@ -165,10 +165,10 @@ void AudioTester::lookForReturnPulse(QVarLengthArray& out_buffer, } // found our impulse // remain pending until timeout, hoping to find our return pulse } // got something - } // loop over samples + } // loop over samples sampleCountSinceImpulse += n_frames; // gets reset to 1 when impulse is found, counts freely until then - } // ImpulsePending + } // ImpulsePending } // Called 2nd in Audiointerface.cpp diff --git a/src/Effects.h b/src/Effects.h index 0332c4e..7a5cb8e 100644 --- a/src/Effects.h +++ b/src/Effects.h @@ -462,11 +462,11 @@ class Effects << "*** Effects.h: parseCompressorArgs: lastParam " << lastParam << " invalid\n"; returnCode = 3; // "reality failure" - } // switch(lastParam) - } // have valid parameter from atof + } // switch(lastParam) + } // have valid parameter from atof } // have valid non-alpha char for parameter - } // switch(ch) - } // for (ulong i=0; i